An application is commonly controlled by a user armed with a keyboard and a mouse. Scripting provides an alternate means of interacting with an application, where one program controls another by sending it special scripting messages. This exposes the functionality of a program in an application- rather than user-oriented manner. Scripting is commonly used for automating repetitive tasks that otherwise would need to be done by hand.
The controlling application sends scripting commands in the form of BMessages to the scriptable application using interapplication BMessengers. More precisely, the application sends scripting commands to specific BHandlers in the target application. In a sense, scripting is already built into every application. For example, you can instruct a window to resize itself by sending it a B_WINDOW_RESIZED message along with appropriate parameters in the "width" and "height" fields.
However, BMessages, BMessengers, and BHandlers alone do not a complete scripting system make. Without standard mechanisms for targeting specific BHandlers and their properties as well as for querying the scripting facilities of an object at run-time, scripting is useless. The BeOS uses the existing messaging system as a base for building a scripting framework.
The scripting framework defines the following notions: commands, properties, and specifiers. If you are familiar with AppleScript, these are equivalent to verbs, nouns, and adjectives. Commands act on a specific instance of a property, as determined by the specifiers.
The command conveys the action of a scripting command and is stored in the what field of the scripting BMessage. There are six standard commands (defined in <be/app/Message.h>):
Each of these commands acts on a "property," which is nothing more than a scriptable feature of an object. As a real world example, the windows owned by an application are properties, as is the title of each window. The particular interpretation of the command depends upon the property it is acting on. For example, B_DELETE_PROPERTY, acting on the "Entry" property of a Tracker window, causes a file to be moved to the trash. However, the same command acting on the "Selection" property of the same window removes files from the list of selected items.
Scriptable objects should limit themselves to this set of commands. If an object uses a nonstandard command, it runs the risk of being unusable by general scripting tools.
A property represents a scriptable feature of an object. Properties are named; these names are strings unique within a class. For example, a BWindow defines properties such as "Frame," "Title," and "View." The data type of the property and its allowable values are determined by the property. For example, the window's "Frame" accepts B_RECT_TYPE values while the "Title" is a B_STRING_TYPE.
Sometimes a property is represented by another object. For example, BWindow's "View" designates a BView, an object which has a set of properties distinct from those of BWindow.
An object may have more than one instance of a given property. For example, the "Window" property of BApplication, has as many instances as there are windows in the application. As a result, there is some ambiguity when you ask for the Window of an application. Instead, it's more correct to ask for the first Window, or the Window named "Snyder." In other words, a property is not enough to identify a feature; a specific instance must be picked out as well.
Specifiers are used to target ("specify") particular instances of properties. A specifier is a BMessage containing the following elements:
There are seven standard specifier constants (defined in <be/app/Message.h>):
As with messages, the precise meaning of a given specifier depends upon the context. Additionally, there may be user-defined (or perhaps more properly object-defined) specifiers. User-defined specifier constants should be greater than B_SPECIFIERS_END to prevent conflicts with Be-defined specifiers.
Specifiers are added to the "specifier" field of a scripting message using BMessage::AddSpecifier(). There are several variants of this method, including shortcuts for adding B_DIRECT_SPECIFIER, B_INDEX_SPECIFIER, B_RANGE_SPECIFIER, and B_NAME_SPECIFIER specifiers. For all other specifiers, you must manually construct the specifier and add it to the scripting message with AddSpecifier(). For example, to add a B_ID_SPECIFIER:
BMessage specifier(B_ID_SPECIFIER); // create a new specifier specifier.AddInt32("id", 2827); // add the id number to the specifier message.AddSpecifier(&specifier); // add the specifier to the message
You must use AddSpecifier() to add specifiers to a BMessage; it performs additional scripting support work that AddMessage() doesn't. |
In general, an application will not be able to obtain a BMessenger for the target object; instead, it'll have to settle for a BMessenger targeting the BApplication of the program containing the desired object. In these cases, a single specifier may be insufficient to target a scripting message. The true power of specifiers lies in their ability to be chained together in the specifier stack.
An example best illustrates the operation of the specifier stack. The following code snippet creates a message that will target the frame of the second view of the window named "egg" in the target application:
message.AddSpecifier("Label"); message.AddSpecifier("MenuBar"); message.AddSpecifier("Window", 1);
Repeated calls to AddSpecifier() build the specifier stack. The order of the calls is very important; the specifiers are evaluated in the opposite order from which they were added. When this message is received by the target application, it will first peel off the third specifier and direct the message to the second window of the application. The BWindow will then peel off the second specifier and direct the message to the window's key menu bar. The first specifier ("Label") is then processed by the BMenuBar. This process is covered in more detail below under ResolveSpecifier().
A reply is generated for every scripting request. The reply message contains the following fields:
Any scriptable objects that you create should also obey the above protocol. Of course, individual objects are free to define their own protocols for relaying additional information in the reply; in these cases, consult the documentation for the class in question.
The scripting facilities of an application can be invoked in three easy steps:
Suppose we want to fetch the frame rectangle of the second view of the window titled "egg" in an application with the signature "application/x-fish". The code:
BMessage message, reply; BRect result; // set the command constant message.what = B_GET_PROPERTY; // construct the specifier stack message.AddSpecifier("Frame"); // B_DIRECT_SPECIFIER message.AddSpecifier("View", 1); // B_INDEX_SPECIFIER message.AddSpecifier("Window", "egg"); // B_NAME_SPECIFIER // send the message and fetch the result BMessenger("application/x-fish").SendMessage(&message, &reply); reply.FindRect("result", &result)
Short and sweet.
There is one missing element in the scripting system, namely the ability to query an object for its scripting abilities. This is useful when the controlling application doesn't know the precise type of the object it is scripting. Having a method of discovering the scripting abilities of an object enables more dynamic uses of scripting.
An object's scripting abilities are organized into one or more scripting "suites," a set of supported messages and associated specifiers. A suite is identified by a MIME-like string with the "suite" supertype. For example, BControl implements the "suite/vnd.Be-control" scripting suite. Nothing prevents two objects from implementing the same suite; two sound editors, for example, could have different implementations of a common scripting suite for filtering audio data.
To ask an object for its supported scripting suites, send it a standard scripting message with a B_GET_PROPERTY request for the "Suites" property:
message.what = B_GET_PROPERTY; message.AddSpecifier("Suites"); ... add remaining specifiers here ... messenger.SendMessage(&message, &reply);
The target object responds with a B_REPLY BMessage with the following fields:
Less usefully, you can send a B_GET_SUPPORTED_SUITES BMessage directly to an object and obtain its supported suites in an identically-formed reply.
Every scriptable object supports the "suite/vnd.Be-handler" suite by dint of its BHandler heritage. This suite is sometimes referred to as the "universal suite." It performs the following functions:
Since scripting messages are passed via BMessengers, objects accepting scripting messages must be derived from BHandler. Typically, adding scripting support entails little more than overriding the following methods:
virtual BHandler *ResolveSpecifier(BMessage *message, int32 index, BMessage *specifier, int32 what, const char *property)
Implemented by derived classes to determine the proper handler for a scripting message. The message is targeted to the BHandler, but the specifiers may indicate that it should be assigned to another object. It's the job of ResolveSpecifier() to examine the current specifier (or more, if necessary) and return the object that should either handle the message or look at the next specifier. This function is called before the message is dispatched and before any filtering functions are called.
The first argument, message, points to the scripting message under consideration. The current specifier is passed in specifier; it will be at index index in the specifier array of message. Finally, what contains the what data member of specifier while property contains the name of the targetted property.
ResolveSpecifier() returns a pointer to the next BHandler that should look at the message. Here, it has four options:
if ( (strcmp(property, "Proxy") == 0) && (what == B_INDEX_SPECIFIER) ) { int32 i; if ( specifier->FindInt32("index", &i) == B_OK ) { MyProxy *proxy = (MyProxy *)proxyList->ItemAt(i); if ( proxy ) { message->PopSpecifier(); if ( proxy->Looper() != Looper() ) { proxy->Looper()->PostMessage(message, proxy); return NULL; } } . . . } . . . }
Since this function resolved the specifier at index, it calls PopSpecifier() to decrement the index before forwarding the message. Otherwise, the next handler would try to resolve the same specifier.
if ( proxy ) { message->PopSpecifier(); if ( proxy->Looper() != Looper() ) { proxy->Looper()->PostMessage(message, proxy); return NULL; } else { return proxy; } }
This, in effect, puts the returned object in the BHandler's place as the designated handler for the message. The BLooper will give the returned handler a chance to respond to the message or resolve the next specifier.
Again, PopSpecifier() should be called so that an attempt isn't made to resolve the same specifier twice.
if ( (strcmp(property, "Value") == 0) && (message->what == B_GET_PROPERTY) ) return this;
This confirms the BHandler as the message target. ResolveSpecifier() won't be called again, so it's not necessary to call PopSpecifier() before returning.
The BApplication object takes the first path when it resolves a specifier for a "Window" property; it sends the message to the specified BWindow and returns NULL. A BWindow follows the second path when it resolves a specifier for a "View" property; it returns the specified BView. Thus, a message initially targeted to the BApplication object can find its way to a BView.
BHandler's version of ResolveSpecifier() recognizes a B_GET_PROPERTY message with a direct specifier requesting a "Suite" for the supported suites, "Messenger" for the BHandler, or the BHandler's "InternalName" (the same name that its Name() function returns). In all three cases, it assigns the BHandler (this) as the object responsible for the message.
For all other specifiers and messages, it sends a B_MESSAGE_NOT_UNDERSTOOD reply and returns NULL. The reply message has an "error" field with B_SCRIPT_SYNTAX as the error and a "message" field with a longer textual explanation of the error.
virtual status_t MessageReceived(BMessage *message)
MessageReceived() is called to process any incoming scripting messages. Scripting messages are treated in this regard much as any other BMessage. MessageReceived() should be implemented to carry out the actions requested by scripting commands.
virtual status_t GetSupportedSuites(BMessage *message)
Implemented by derived classes to report the suites of messages and specifiers they understand. This function is called in response to either a B_GET_PROPERTIES scripting message for the "Suites" property or a B_GET_SUPPORTED_SUITES message.
Each derived class should add the names of the suites it implements to the "suites" array of message. Each item in the array is a MIME-like string with the "suite" supertype. In addition, the class should add corresponding flattened BPropertyInfo objects in the "messages" array. A typical implementation of GetSupportedSuites() looks like:
status_t MyHandler::GetSupportedSuites(BMessage *message) { message->AddString("suites", "suite/vnd.Me-my_handler")); BPropertyInfo prop_info(prop_list); message->AddFlat("messages", &prop_info); return BHandler::GetSupportedSuites(message); }
The value returned by GetSupportedSuites() is added to message in the int32 "error" field. BHandler's version of this function adds the universal suite "suite/vnd.Be-handler" to message then returns B_OK.
The Be Book, in lovely HTML, for BeOS Release 4.
Copyright © 1998 Be, Inc. All rights reserved.
Last modified December 23, 1998.